Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit f62b203bebdec1de31991f198904a10b7cd73e62


Parents : 0c80fef
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-10T13:02:33-05:00

refactor(docs): update Reticulum manual fetching process to remove GitHub update functionality and ensure build-time fetching with --force option

Changes
Diff

diff --git a/docs/en/installation.md b/docs/en/installation.md
index 2d55425a..0204cf0e 100644
--- a/docs/en/installation.md
+++ b/docs/en/installation.md
@@ -126,13 +126,13 @@ CLI flags override environment variables when both are set.
## Reticulum manual bundle
-The Reticulum HTML manual is fetched at build time. After cloning the repository, run:
+The Reticulum HTML manual is fetched from the upstream website **master** branch at build time. There is no in-app clearnet refresh. After cloning the repository, or before packaging a release, run:
```bash
pnpm run build-docs
```
-This populates `meshchatx/public/reticulum-docs-bundled/current/`. Without that step the Reticulum tab may show an upload prompt until you build docs or upload a manual ZIP.
+That command always re-fetches (`--force`) into `meshchatx/public/reticulum-docs-bundled/current/`. CI release builds run the same step. Without a bundled copy the Reticulum tab may show an upload prompt until you build docs or upload a manual ZIP offline.
## Identity bootstrap

diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index aa4f0f72..85f79448 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -7808,27 +7808,6 @@ class ReticulumMeshChat:
except Exception as e:
return web.json_response({"error": str(e)}, status=500)
- # pull latest Reticulum manual from GitHub
- @routes.post("/api/v1/docs/update-from-github")
- async def docs_update_from_github(request):
- try:
- self._require_outbound_http("Reticulum docs update")
- if not self.docs_manager:
- return web.json_response(
- {"error": "Documentation manager is unavailable"},
- status=503,
- )
- loop = asyncio.get_running_loop()
- success, version = await loop.run_in_executor(
- None,
- self.docs_manager.update_from_github,
- )
- return web.json_response({"success": success, "version": version})
- except OutboundHttpBlockedError as e:
- return web.json_response({"error": str(e)}, status=403)
- except Exception as e:
- return web.json_response({"error": str(e)}, status=500)
-
# switch docs version
@routes.post("/api/v1/docs/switch")
async def docs_switch(request):

diff --git a/meshchatx/src/backend/docs_manager.py b/meshchatx/src/backend/docs_manager.py
index 4bbf3274..c593e4b9 100644
--- a/meshchatx/src/backend/docs_manager.py
+++ b/meshchatx/src/backend/docs_manager.py
@@ -7,10 +7,6 @@ import logging
import os
import re
import shutil
-import time
-import urllib.error
-import urllib.parse
-import urllib.request
import zipfile
from meshchatx.src.backend.markdown_renderer import MarkdownRenderer
@@ -18,16 +14,6 @@ from meshchatx.src.backend.markdown_renderer import MarkdownRenderer
BUNDLED_DOCS_SUBDIR = os.path.join("reticulum-docs-bundled", "current")
MANIFEST_FILENAME = "manifest.json"
DOC_FILE_SUFFIXES = (".md", ".txt")
-RETICULUM_DOCS_GITHUB_URL = (
- "https://github.com/markqvist/reticulum_website/archive/refs/heads/main.zip"
-)
-RETICULUM_DOCS_ALLOWED_HOSTS = frozenset(
- {
- "github.com",
- "codeload.github.com",
- "objects.githubusercontent.com",
- }
-)
class DocsManager:
@@ -37,9 +23,8 @@ class DocsManager:
``<public_dir>/reticulum-docs-bundled/current``. Users may upload a
replacement archive which is extracted into ``<storage_dir>/reticulum-docs``
and takes precedence at request time. Removing the user upload restores the
- bundled copy. Users can also pull the latest upstream ZIP from GitHub at
- runtime (blocked when privacy mode is enabled). Build-time staging still
- uses ``scripts/build/fetch_reticulum_manual.py``.
+ bundled copy. There is no runtime download path. Fresh manuals are staged at
+ build time with ``scripts/build/fetch_reticulum_manual.py`` (``pnpm run build-docs``).
"""
def __init__(self, config, public_dir, project_root=None, storage_dir=None):
@@ -838,50 +823,6 @@ class DocsManager:
logging.exception(f"Failed to upload docs: {e}")
return False
- @staticmethod
- def _resolve_docs_source_url(source_url=None):
- url = (
- source_url
- or os.environ.get("MESHCHATX_RETICULUM_DOCS_URL")
- or RETICULUM_DOCS_GITHUB_URL
- ).strip()
- if not url.lower().startswith("https://"):
- raise ValueError("docs source URL must be https")
- host = urllib.parse.urlparse(url).hostname or ""
- host = host.lower()
- if host not in RETICULUM_DOCS_ALLOWED_HOSTS and not host.endswith(
- ".githubusercontent.com"
- ):
- raise ValueError(f"docs source host not allowed: {host}")
- return url
-
- def update_from_github(self, version=None, source_url=None, timeout=120.0):
- """Download the Reticulum website docs ZIP and install it as a version."""
- url = self._resolve_docs_source_url(source_url)
- if not version:
- version = f"github-{time.strftime('%Y%m%d-%H%M%S')}"
-
- self.upload_status = "downloading"
- self.upload_progress = 0
- self.last_error = None
-
- try:
- req = urllib.request.Request(
- url,
- headers={"User-Agent": "MeshChatX-docs-update"},
- )
- with urllib.request.urlopen(req, timeout=timeout) as response:
- zip_bytes = response.read()
- if not zip_bytes:
- raise ValueError("downloaded archive is empty")
- success = self.upload_zip(zip_bytes, version)
- return success, version
- except Exception as e:
- self.last_error = str(e)
- self.upload_status = "error"
- logging.exception(f"Failed to update docs from GitHub: {e}")
- raise
-
def _extract_docs(self, zip_path, version):
safe_version = os.path.basename(version)
if not safe_version or safe_version in (".", ".."):

diff --git a/meshchatx/src/frontend/components/docs/DocsPage.vue b/meshchatx/src/frontend/components/docs/DocsPage.vue
index e4c08c9d..60ceeefc 100644
--- a/meshchatx/src/frontend/components/docs/DocsPage.vue
+++ b/meshchatx/src/frontend/components/docs/DocsPage.vue
@@ -186,22 +186,6 @@
<MaterialDesignIcon icon-name="download" class="w-4 h-4 md:w-5 md:h-5" />
</button>
- <!-- Update from GitHub -->
- <button
- type="button"
- class="p-1.5 text-gray-500 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded-lg transition-colors"
- :class="{ 'opacity-50 pointer-events-none': docsBusy }"
- :title="$t('docs.btn_update_github')"
- :disabled="docsBusy"
- @click="updateFromGithub"
- >
- <MaterialDesignIcon
- :icon-name="docsBusy ? 'loading' : 'update'"
- :class="{ 'animate-spin': docsBusy }"
- class="w-4 h-4 md:w-5 md:h-5"
- />
- </button>
-
<!-- Share Reticulum Manual (re-uploadable ZIP) -->
<button
v-if="status.has_docs"
@@ -214,16 +198,22 @@
<!-- Upload Custom Manual -->
<label
- :class="{ 'opacity-50 pointer-events-none': docsBusy }"
+ :class="{ 'opacity-50 pointer-events-none': status.status === 'extracting' }"
class="p-1.5 text-gray-500 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded-lg transition-colors cursor-pointer"
:title="$t('docs.btn_upload')"
>
<MaterialDesignIcon
- :icon-name="docsBusy ? 'loading' : 'upload'"
- :class="{ 'animate-spin': docsBusy }"
+ :icon-name="status.status === 'extracting' ? 'loading' : 'upload'"
+ :class="{ 'animate-spin': status.status === 'extracting' }"
class="w-4 h-4 md:w-5 md:h-5"
/>
- <input type="file" accept=".zip" class="hidden" :disabled="docsBusy" @change="handleZipUpload" />
+ <input
+ type="file"
+ accept=".zip"
+ class="hidden"
+ :disabled="status.status === 'extracting'"
+ @change="handleZipUpload"
+ />
</label>
<!-- Open External -->
@@ -301,7 +291,10 @@
</div>
<!-- Progress Bar -->
- <div v-if="docsBusy" class="w-full h-1 bg-gray-200 dark:bg-zinc-800 overflow-hidden relative">
+ <div
+ v-if="status.status === 'extracting'"
+ class="w-full h-1 bg-gray-200 dark:bg-zinc-800 overflow-hidden relative"
+ >
<div class="bg-blue-500 h-full transition-all duration-300" :style="{ width: status.progress + '%' }"></div>
<div class="absolute inset-0 bg-blue-500/30 animate-pulse"></div>
</div>
@@ -422,7 +415,7 @@
</div>
<div
- v-if="docsBusy"
+ v-if="status.status === 'extracting'"
class="absolute inset-0 z-10 flex flex-col items-center justify-center bg-white/80 dark:bg-zinc-900/80 backdrop-blur-md"
>
<div class="relative w-24 h-24 mb-6">
@@ -434,13 +427,13 @@
></div>
<div class="absolute inset-0 flex items-center justify-center">
<MaterialDesignIcon
- :icon-name="status.status === 'downloading' ? 'cloud-download' : 'folder-zip-outline'"
+ icon-name="folder-zip-outline"
class="w-10 h-10 text-blue-600 animate-bounce"
/>
</div>
</div>
<h3 class="text-lg font-bold text-gray-900 dark:text-zinc-100 mb-1">
- {{ status.status === "downloading" ? $t("docs.status_downloading") : $t("docs.status_extracting") }}
+ {{ $t("docs.status_extracting") }}
</h3>
<p class="text-sm text-gray-500 dark:text-zinc-400">
{{ $t("docs.complete_percent", { percent: status.progress }) }}
@@ -617,7 +610,9 @@
></iframe>
<div
- v-else-if="activeTab === 'reticulum' && !status.has_docs && !docsBusy && !searchQuery"
+ v-else-if="
+ activeTab === 'reticulum' && !status.has_docs && status.status !== 'extracting' && !searchQuery
+ "
class="h-full flex flex-col items-center justify-center p-8 text-center space-y-4"
>
<div class="w-16 h-16 bg-gray-50 dark:bg-zinc-800/50 rounded-full flex items-center justify-center">
@@ -631,24 +626,13 @@
{{ $t("docs.empty_state_hint") }}
</p>
</div>
- <div class="flex flex-col sm:flex-row gap-2">
- <button
- type="button"
- class="px-6 py-2 bg-blue-600 text-white rounded-full text-xs font-bold hover:bg-blue-700 transition-colors shadow-lg shadow-blue-500/20 flex items-center justify-center gap-2"
- :disabled="docsBusy"
- @click="updateFromGithub"
- >
- <MaterialDesignIcon icon-name="update" class="w-3.5 h-3.5" />
- <span>{{ $t("docs.btn_update_github") }}</span>
- </button>
- <label
- class="px-6 py-2 bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 rounded-full text-xs font-bold hover:opacity-90 transition-opacity cursor-pointer flex items-center justify-center gap-2"
- >
- <MaterialDesignIcon icon-name="upload" class="w-3.5 h-3.5" />
- <span>{{ $t("docs.btn_upload") }}</span>
- <input type="file" accept=".zip" class="hidden" @change="handleZipUpload" />
- </label>
- </div>
+ <label
+ class="px-6 py-2 bg-blue-600 text-white rounded-full text-xs font-bold hover:bg-blue-700 transition-colors shadow-lg shadow-blue-500/20 cursor-pointer flex items-center gap-2"
+ >
+ <MaterialDesignIcon icon-name="upload" class="w-3.5 h-3.5" />
+ <span>{{ $t("docs.btn_upload") }}</span>
+ <input type="file" accept=".zip" class="hidden" @change="handleZipUpload" />
+ </label>
</div>
</div>
</div>
@@ -699,7 +683,7 @@ export default {
selectedDocPath: null,
selectedDocContent: null,
selectedReticulumPath: null,
- githubUpdatePending: false,
+ reticulumDocsCacheBust: 0,
languages: {
en: "English",
de: "Deutsch",
@@ -719,10 +703,17 @@ export default {
return this.$i18n.locale;
},
localDocsUrl() {
+ let path;
if (this.selectedReticulumPath) {
- return `/reticulum-docs/${this.selectedReticulumPath}`;
+ path = `/reticulum-docs/${this.selectedReticulumPath}`;
+ } else {
+ path = bundledReticulumDocsUrl(this.currentLang);
+ }
+ if (this.reticulumDocsCacheBust) {
+ const sep = path.includes("?") ? "&" : "?";
+ return `${path}${sep}v=${this.reticulumDocsCacheBust}`;
}
- return bundledReticulumDocsUrl(this.currentLang);
+ return path;
},
allLanguages() {
return Object.entries(this.languages).map(([code, name]) => ({
@@ -737,11 +728,6 @@ export default {
reticulumDocsQueryParam() {
return this.$route?.query?.reticulum;
},
- docsBusy() {
- return (
- this.githubUpdatePending || this.status.status === "downloading" || this.status.status === "extracting"
- );
- },
visibleDocSections() {
const lang = this.meshchatxDocsLang;
const fallback = this.defaultDocsLanguage || "en";
@@ -963,6 +949,7 @@ export default {
}
);
this.fetchStatus();
+ this.reticulumDocsCacheBust = Date.now();
ToastUtils.success(this.$t("docs.upload_success"));
} catch (error) {
console.error("Failed to upload docs zip:", error);
@@ -970,37 +957,6 @@ export default {
DialogUtils.alert(this.$t("docs.failed_upload_alert", { message }), "error");
}
},
- async updateFromGithub() {
- if (this.docsBusy) {
- return;
- }
- this.githubUpdatePending = true;
- this.status = {
- ...this.status,
- status: "downloading",
- progress: 0,
- last_error: null,
- };
- try {
- const response = await window.api.post("/api/v1/docs/update-from-github");
- const version = response.data?.version;
- await this.fetchStatus();
- this.activeTab = "reticulum";
- this.selectedReticulumPath = null;
- ToastUtils.success(
- version
- ? this.$t("docs.update_github_success_version", { version })
- : this.$t("docs.update_github_success")
- );
- } catch (error) {
- console.error("Failed to update docs from GitHub:", error);
- const message = error.response?.data?.error || error.message || "";
- DialogUtils.alert(this.$t("docs.failed_update_github", { message }), "error");
- await this.fetchStatus();
- } finally {
- this.githubUpdatePending = false;
- }
- },
async exportDocs() {
window.location.href = "/api/v1/docs/export";
},

diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json
index de4c7577..f5dd11a4 100644
--- a/meshchatx/src/frontend/locales/de.json
+++ b/meshchatx/src/frontend/locales/de.json
@@ -1967,19 +1967,14 @@
"prompt_version_name": "Versionsname für diesen Upload eingeben:",
"status_title": "Dokumentationsstatus",
"status_extracting": "Wird entpackt...",
- "status_downloading": "Dokumentation wird heruntergeladen...",
"status_available": "Offline-Handbuch verfügbar",
"status_not_available": "Handbuch nicht verfügbar",
"btn_upload": "Handbuch hochladen",
- "btn_update_github": "Von GitHub aktualisieren",
"btn_share": "Reticulum-Handbuch als wieder hochladbares ZIP teilen",
- "empty_state_hint": "Lade das neueste Reticulum-Handbuch von GitHub, oder lade ein ZIP hoch, um es offline anzuzeigen.",
+ "empty_state_hint": "Lade ein ZIP des Reticulum-Handbuchs hoch, um es offline anzuzeigen. Releases enthalten eine gebündelte Offline-Kopie aus dem Build.",
"error": "Fehler",
"failed_upload_docs": "Hochladen der Dokumentation fehlgeschlagen",
"upload_success": "Dokumentation hochgeladen",
- "update_github_success": "Reticulum-Handbuch von GitHub aktualisiert",
- "update_github_success_version": "Reticulum-Handbuch aktualisiert ({version})",
- "failed_update_github": "Aktualisierung von GitHub fehlgeschlagen: {message}",
"docs_link_copied": "Dokumentationslink in Zwischenablage kopiert",
"failed_copy_link": "Link kopieren fehlgeschlagen",
"load_list_failed": "Anleitungsliste konnte nicht geladen werden. Bitte später erneut versuchen.",

diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json
index b5ef2c78..395973a3 100644
--- a/meshchatx/src/frontend/locales/en.json
+++ b/meshchatx/src/frontend/locales/en.json
@@ -2087,23 +2087,14 @@
"status_available": "Offline Manual Available",
"status_not_available": "Manual Not Available",
"btn_upload": "Upload Manual",
- "btn_update_github": "Update from GitHub",
- "btn_share": "Share Reticulum manual as a re-uploadable ZIP",
- "empty_state_hint": "Pull the latest Reticulum manual from GitHub, or upload a ZIP to view it offline.",
- "error": "Error",
- "failed_upload_docs": "Failed to upload documentation",
- "upload_success": "Documentation uploaded",
- "update_github_success": "Reticulum manual updated from GitHub",
- "update_github_success_version": "Reticulum manual updated ({version})",
- "failed_update_github": "Failed to update from GitHub: {message}",
- "status_downloading": "Downloading Documentation...",
"docs_link_copied": "Documentation link copied to clipboard",
"failed_copy_link": "Failed to copy link",
"load_list_failed": "Could not load the guide list. Try again in a moment.",
"load_doc_failed": "Could not load this guide.",
"search_failed": "Search failed. Check your connection and try again.",
"manifest_warning": "The documentation index file could not be read. Showing available files without section grouping.",
- "failed_upload_alert": "Failed to upload documentation: {message}"
+ "failed_upload_alert": "Failed to upload documentation: {message}",
+ "empty_state_hint": "Upload a Reticulum manual ZIP to view it offline. Releases ship a bundled offline copy built at packaging time."
},
"licenses": {
"section_label": "Legal",

diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json
index cc6aa30b..cb42a768 100644
--- a/meshchatx/src/frontend/locales/es.json
+++ b/meshchatx/src/frontend/locales/es.json
@@ -2084,19 +2084,14 @@
"prompt_version_name": "Introduce un nombre de versión para esta subida:",
"status_title": "Situación de la documentación",
"status_extracting": "Extrayendo...",
- "status_downloading": "Descargando documentación...",
"status_available": "Manual disponible",
"status_not_available": "Manual no disponible",
"btn_upload": "Subir Manual",
- "btn_update_github": "Actualizar desde GitHub",
"btn_share": "Compartir el manual de Reticulum como ZIP reimportable",
- "empty_state_hint": "Descarga el manual de Reticulum más reciente desde GitHub, o sube un ZIP para verlo sin conexión.",
+ "empty_state_hint": "Sube un ZIP del manual de Reticulum para consultarlo sin conexión. Las versiones incluyen una copia sin conexión empaquetada en el build.",
"error": "Error",
"failed_upload_docs": "Error al subir la documentación",
"upload_success": "Documentación subida",
- "update_github_success": "Manual de Reticulum actualizado desde GitHub",
- "update_github_success_version": "Manual de Reticulum actualizado ({version})",
- "failed_update_github": "Error al actualizar desde GitHub: {message}",
"docs_link_copied": "Enlace de documentación copiado al portapapeles",
"failed_copy_link": "No se pudo copiar el enlace",
"load_list_failed": "No se pudo cargar la lista de guías. Inténtalo de nuevo en un momento.",

diff --git a/meshchatx/src/frontend/locales/fi.json b/meshchatx/src/frontend/locales/fi.json
index 4333435f..961e6a78 100644
--- a/meshchatx/src/frontend/locales/fi.json
+++ b/meshchatx/src/frontend/locales/fi.json
@@ -2084,19 +2084,14 @@
"prompt_version_name": "Anna tälle lataukselle version nimi:",
"status_title": "Dokumentaation tila",
"status_extracting": "Viedään dokumentaatiota...",
- "status_downloading": "Ladataan dokumentaatiota...",
"status_available": "Paikallinen ohjekirja saatavilla",
"status_not_available": "Ohjekirja ei ole saatavilla",
"btn_upload": "Tuo ohjekirja",
- "btn_update_github": "Päivitä GitHubista",
"btn_share": "Jaa Reticulum-ohjekirja ZIP-tiedostona, jonka voi myöhemmin tuoda sovellukseen",
- "empty_state_hint": "Hae uusin Reticulum-ohjekirja GitHubista tai tuo ZIP paikallista käyttöä varten.",
+ "empty_state_hint": "Tuo Reticulum-ohjekirjan ZIP paikallista käyttöä varten. Julkaisut sisältävät buildissa pakatun offline-kopion.",
"error": "Virhe",
"failed_upload_docs": "Dokumentaation tuominen epäonnistui",
"upload_success": "Dokumentaatio ladattu",
- "update_github_success": "Reticulum-ohjekirja päivitetty GitHubista",
- "update_github_success_version": "Reticulum-ohjekirja päivitetty ({version})",
- "failed_update_github": "Päivitys GitHubista epäonnistui: {message}",
"docs_link_copied": "Dokumentaation linkki kopioitu leikepöydälle",
"failed_copy_link": "Linkin kopiointi epäonnistui",
"load_list_failed": "Ohjeluetteloa ei voitu ladata. Yritä hetken kuluttua uudelleen.",

diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json
index 35719f14..f6c3b860 100644
--- a/meshchatx/src/frontend/locales/fr.json
+++ b/meshchatx/src/frontend/locales/fr.json
@@ -2084,19 +2084,14 @@
"prompt_version_name": "Entrez un nom de version pour ce téléversement :",
"status_title": "État de la documentation",
"status_extracting": "Extraction...",
- "status_downloading": "Téléchargement de la documentation...",
"status_available": "Manuel hors ligne disponible",
"status_not_available": "Manuel non disponible",
"btn_upload": "Téléverser le manuel",
- "btn_update_github": "Mettre à jour depuis GitHub",
"btn_share": "Partager le manuel Reticulum sous forme de ZIP réimportable",
- "empty_state_hint": "Récupérez le dernier manuel Reticulum depuis GitHub, ou téléversez une archive ZIP pour le consulter hors ligne.",
+ "empty_state_hint": "Téléversez une archive ZIP du manuel Reticulum pour la consulter hors ligne. Les versions livrent une copie hors ligne intégrée au build.",
"error": "Erreur",
"failed_upload_docs": "Échec du téléversement de la documentation",
"upload_success": "Documentation téléversée",
- "update_github_success": "Manuel Reticulum mis à jour depuis GitHub",
- "update_github_success_version": "Manuel Reticulum mis à jour ({version})",
- "failed_update_github": "Échec de la mise à jour depuis GitHub : {message}",
"docs_link_copied": "Lien de documentation copié dans le presse-papiers",
"failed_copy_link": "Impossible de copier le lien",
"load_list_failed": "Impossible de charger la liste des guides. Réessayez dans un instant.",

diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json
index d7758794..f33e9b0b 100644
--- a/meshchatx/src/frontend/locales/it.json
+++ b/meshchatx/src/frontend/locales/it.json
@@ -2136,19 +2136,14 @@
"prompt_version_name": "Inserisci un nome di versione per questo caricamento:",
"status_title": "Stato Documentazione",
"status_extracting": "Estrazione in corso...",
- "status_downloading": "Download della documentazione...",
"status_available": "Manuale Offline Disponibile",
"status_not_available": "Manuale Non Disponibile",
"btn_upload": "Carica Manuale",
- "btn_update_github": "Aggiorna da GitHub",
"btn_share": "Condividi il manuale Reticulum come ZIP ricaricabile",
- "empty_state_hint": "Scarica l'ultimo manuale Reticulum da GitHub, oppure carica uno ZIP per consultarlo offline.",
+ "empty_state_hint": "Carica uno ZIP del manuale Reticulum per consultarlo offline. Le release includono una copia offline inclusa nel build.",
"error": "Errore",
"failed_upload_docs": "Caricamento della documentazione non riuscito",
"upload_success": "Documentazione caricata",
- "update_github_success": "Manuale Reticulum aggiornato da GitHub",
- "update_github_success_version": "Manuale Reticulum aggiornato ({version})",
- "failed_update_github": "Aggiornamento da GitHub non riuscito: {message}",
"docs_link_copied": "Link alla documentazione copiato negli appunti",
"failed_copy_link": "Impossibile copiare il link",
"load_list_failed": "Impossibile caricare l'elenco delle guide. Riprova tra un momento.",

diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json
index dedbba5c..9f70b2af 100644
--- a/meshchatx/src/frontend/locales/nl.json
+++ b/meshchatx/src/frontend/locales/nl.json
@@ -2084,19 +2084,14 @@
"prompt_version_name": "Voer een versienaam in voor deze upload:",
"status_title": "Documentatiestatus",
"status_extracting": "Uitpakken...",
- "status_downloading": "Documentatie downloaden...",
"status_available": "Offline handleiding beschikbaar",
"status_not_available": "Handleiding niet beschikbaar",
"btn_upload": "Handleiding uploaden",
- "btn_update_github": "Bijwerken vanaf GitHub",
"btn_share": "Reticulum-handleiding delen als opnieuw uploadbare ZIP",
- "empty_state_hint": "Haal de nieuwste Reticulum-handleiding van GitHub, of upload een ZIP om deze offline te bekijken.",
+ "empty_state_hint": "Upload een ZIP van de Reticulum-handleiding om deze offline te bekijken. Releases bevatten een gebundelde offline kopie uit de build.",
"error": "Fout",
"failed_upload_docs": "Documentatie uploaden mislukt",
"upload_success": "Documentatie geüpload",
- "update_github_success": "Reticulum-handleiding bijgewerkt vanaf GitHub",
- "update_github_success_version": "Reticulum-handleiding bijgewerkt ({version})",
- "failed_update_github": "Bijwerken vanaf GitHub mislukt: {message}",
"docs_link_copied": "Documentatiekoppeling gekopieerd naar klembord",
"failed_copy_link": "Kopiëren van verwijzing is mislukt",
"load_list_failed": "Kan de gidsenlijst niet laden. Probeer het zo opnieuw.",

diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json
index 1cbd69a4..159f1e55 100644
--- a/meshchatx/src/frontend/locales/ru.json
+++ b/meshchatx/src/frontend/locales/ru.json
@@ -1967,19 +1967,14 @@
"prompt_version_name": "Введите имя версии для этой загрузки:",
"status_title": "Статус документации",
"status_extracting": "Извлечение...",
- "status_downloading": "Загрузка документации...",
"status_available": "Доступно офлайн",
"status_not_available": "Руководство недоступно",
"btn_upload": "Загрузить руководство",
- "btn_update_github": "Обновить с GitHub",
"btn_share": "Поделиться руководством Reticulum как загружаемым ZIP",
- "empty_state_hint": "Загрузите актуальное руководство Reticulum с GitHub или загрузите ZIP для офлайн-просмотра.",
+ "empty_state_hint": "Загрузите ZIP руководства Reticulum для офлайн-просмотра. В релизах есть встроенная офлайн-копия из сборки.",
"error": "Ошибка",
"failed_upload_docs": "Не удалось загрузить документацию",
"upload_success": "Документация загружена",
- "update_github_success": "Руководство Reticulum обновлено с GitHub",
- "update_github_success_version": "Руководство Reticulum обновлено ({version})",
- "failed_update_github": "Не удалось обновить с GitHub: {message}",
"docs_link_copied": "Ссылка на документацию скопирована в буфер обмена",
"failed_copy_link": "Не удалось скопировать ссылку",
"load_list_failed": "Не удалось загрузить список справок. Повторите попытку позже.",

diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json
index 7c146be4..d8ba3df2 100644
--- a/meshchatx/src/frontend/locales/zh.json
+++ b/meshchatx/src/frontend/locales/zh.json
@@ -2084,19 +2084,14 @@
"prompt_version_name": "为此上传输入版本名称:",
"status_title": "文件状况",
"status_extracting": "正在解压...",
- "status_downloading": "正在下载文档...",
"status_available": "离线手册可用",
"status_not_available": "手册不可用",
"btn_upload": "上传手册",
- "btn_update_github": "从 GitHub 更新",
"btn_share": "以可重新上传的 ZIP 共享 Reticulum 手册",
- "empty_state_hint": "从 GitHub 拉取最新 Reticulum 手册,或上传 ZIP 以便离线查看。",
+ "empty_state_hint": "上传 Reticulum 手册 ZIP 以便离线查看。发行版在构建时打包离线副本。",
"error": "错误",
"failed_upload_docs": "上传文档失败",
"upload_success": "文档已上传",
- "update_github_success": "已从 GitHub 更新 Reticulum 手册",
- "update_github_success_version": "已更新 Reticulum 手册({version})",
- "failed_update_github": "从 GitHub 更新失败:{message}",
"docs_link_copied": "复制到剪贴板的文档链接",
"failed_copy_link": "复制链接失败",
"load_list_failed": "无法加载指南列表。请稍后再试。",

diff --git a/meshchatx/src/frontend/public/meshchatx-docs/en/installation.md b/meshchatx/src/frontend/public/meshchatx-docs/en/installation.md
index 2d55425a..0204cf0e 100644
--- a/meshchatx/src/frontend/public/meshchatx-docs/en/installation.md
+++ b/meshchatx/src/frontend/public/meshchatx-docs/en/installation.md
@@ -126,13 +126,13 @@ CLI flags override environment variables when both are set.
## Reticulum manual bundle
-The Reticulum HTML manual is fetched at build time. After cloning the repository, run:
+The Reticulum HTML manual is fetched from the upstream website **master** branch at build time. There is no in-app clearnet refresh. After cloning the repository, or before packaging a release, run:
```bash
pnpm run build-docs
```
-This populates `meshchatx/public/reticulum-docs-bundled/current/`. Without that step the Reticulum tab may show an upload prompt until you build docs or upload a manual ZIP.
+That command always re-fetches (`--force`) into `meshchatx/public/reticulum-docs-bundled/current/`. CI release builds run the same step. Without a bundled copy the Reticulum tab may show an upload prompt until you build docs or upload a manual ZIP offline.
## Identity bootstrap

diff --git a/package.json b/package.json
index 9774e84e..2dea7751 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,7 @@
"predev": "node scripts/sync-meshchatx-docs.js",
"build-frontend": "vite build",
"build-backend": "node scripts/build-backend.js",
- "build-docs": "python3 scripts/build/fetch_reticulum_manual.py",
+ "build-docs": "python3 scripts/build/fetch_reticulum_manual.py --force",
"build-repository-wheels": "python3 scripts/build/fetch_repository_wheels.py",
"version:sync": "node scripts/sync_version.js",
"build": "pnpm run version:sync && pnpm run build-frontend && pnpm run build-docs && pnpm run build-repository-wheels && pnpm run build-backend",

diff --git a/scripts/build/fetch_reticulum_manual.py b/scripts/build/fetch_reticulum_manual.py
index 7c96696d..e53d4bf1 100755
--- a/scripts/build/fetch_reticulum_manual.py
+++ b/scripts/build/fetch_reticulum_manual.py
@@ -39,7 +39,7 @@ from datetime import UTC, datetime
from pathlib import Path
DEFAULT_SOURCES = (
- "https://github.com/markqvist/reticulum_website/archive/refs/heads/main.zip",
+ "https://codeload.github.com/markqvist/reticulum_website/zip/refs/heads/master",
)
DEFAULT_DEST = (

diff --git a/scripts/build/reticulum_docs_bundle.json b/scripts/build/reticulum_docs_bundle.json
index ee876ddb..dded1e5f 100644
--- a/scripts/build/reticulum_docs_bundle.json
+++ b/scripts/build/reticulum_docs_bundle.json
@@ -1,7 +1,7 @@
{
"dest": "meshchatx/public/reticulum-docs-bundled/current",
- "fetched_utc": "2026-07-07T05:31:03Z",
- "html_files": 201,
+ "fetched_utc": "2026-07-10T17:45:25Z",
+ "html_files": 206,
"skipped_binary_files": 2,
- "source_url": "https://github.com/markqvist/reticulum_website/archive/refs/heads/main.zip"
+ "source_url": "https://codeload.github.com/markqvist/reticulum_website/zip/refs/heads/master"
}

diff --git a/tests/backend/fixtures/http_api_routes.json b/tests/backend/fixtures/http_api_routes.json
index 80d63f75..6d686ebd 100644
--- a/tests/backend/fixtures/http_api_routes.json
+++ b/tests/backend/fixtures/http_api_routes.json
@@ -268,10 +268,6 @@
"method": "POST",
"path": "/api/v1/docs/upload"
},
- {
- "method": "POST",
- "path": "/api/v1/docs/update-from-github"
- },
{
"method": "DELETE",
"path": "/api/v1/docs/version/{version}"

diff --git a/tests/backend/test_docs_manager.py b/tests/backend/test_docs_manager.py
index 6a703f1c..5bf1eafc 100644
--- a/tests/backend/test_docs_manager.py
+++ b/tests/backend/test_docs_manager.py
@@ -544,47 +544,3 @@ def test_get_doc_content_returns_none_on_read_error(tmp_path, monkeypatch):
monkeypatch.setattr("builtins.open", fail_open)
assert dm.get_doc_content("en/guide.md") is None
-
-
-def test_resolve_docs_source_url_rejects_non_https():
- with pytest.raises(ValueError, match="https"):
- DocsManager._resolve_docs_source_url(
- "http://github.com/markqvist/reticulum_website/archive/refs/heads/main.zip"
- )
-
-
-def test_resolve_docs_source_url_rejects_unknown_host():
- with pytest.raises(ValueError, match="not allowed"):
- DocsManager._resolve_docs_source_url("https://evil.example/docs.zip")
-
-
-def test_update_from_github_downloads_and_installs(docs_manager, monkeypatch):
- payload = _make_docs_zip(
- files={
- "reticulum_website-main/docs/index.html": "<html>github</html>",
- },
- )
-
- class FakeResponse:
- def __enter__(self):
- return self
-
- def __exit__(self, *_args):
- return False
-
- def read(self):
- return payload
-
- monkeypatch.setattr(
- "meshchatx.src.backend.docs_manager.urllib.request.urlopen",
- lambda *_args, **_kwargs: FakeResponse(),
- )
-
- success, version = docs_manager.update_from_github(version="github-test")
- assert success is True
- assert version == "github-test"
- assert docs_manager.upload_status == "completed"
- resolved = docs_manager.find_docs_file("index.html")
- assert resolved is not None
- with open(resolved) as fh:
- assert "github" in fh.read()


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────